package imgutil

import (
	"bytes"
	"encoding/base64"
	"errors"
	"image"
	"image/gif"
	"image/jpeg"
	"image/png"
	"io"
	"os"
	"regexp"
	"strings"
	"unicode/utf8"
)

type imgMeta struct {
	imgType string
	offset  int
	bytes   []byte
	offset2 int
	bytes2  []byte
}

// 图片类型与magic number对应关系
var metas []imgMeta = []imgMeta{
	// most popular types
	{
		"png",
		0,
		[]byte{0x89, 0x50, 0x4e, 0x47},
		-1,
		nil,
	},
	{
		"jpg",
		0,
		[]byte{0xff, 0xd8, 0xff, 0xe0},
		-1,
		nil,
	},
	{
		"webp",
		0,
		[]byte{0x52, 0x49, 0x46, 0x46},
		8,
		[]byte{0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38},
	},
	{
		"gif",
		0,
		[]byte{0x47, 0x49, 0x46, 0x38},
		-1,
		nil,
	},
	{
		"bmp",
		0,
		[]byte{0x42, 0x4d},
		-1,
		nil,
	},
	{
		"ico",
		0,
		[]byte{0x00, 0x00, 0x01, 0x00},
		-1,
		nil,
	},
	// less popular types
	{
		"tif",
		0,
		[]byte{0x4d, 0x4d, 0x00, 0x2a},
		-1,
		nil,
	},
	{
		"tif",
		0,
		[]byte{0x49, 0x49, 0x2a, 0x00},
		-1,
		nil,
	},
	{
		"fits",
		0,
		[]byte{0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45},
		-1,
		nil,
	},
	{
		"gks",
		0,
		[]byte{0x47, 0x4b, 0x53, 0x4d},
		-1,
		nil,
	},
	{
		"rgb",
		0,
		[]byte{0x01, 0xda},
		-1,
		nil,
	},
	{
		"itc",
		0,
		[]byte{0xf1, 0x00, 0x40, 0xbb},
		-1,
		nil,
	},
	{
		"nif",
		0,
		[]byte{0x49, 0x49, 0x4e, 0x31},
		-1,
		nil,
	},
	{
		"pm",
		0,
		[]byte{0x56, 0x49, 0x45, 0x57},
		-1,
		nil,
	},
	{
		"eps",
		0,
		[]byte{0x25, 0x21},
		-1,
		nil,
	},
	{
		"ras",
		0,
		[]byte{0x59, 0xa6, 0x6a, 0x95},
		-1,
		nil,
	},
	{
		"xcf",
		0,
		[]byte{0x67, 0x69, 0x6d, 0x70, 0x20, 0x78, 0x63, 0x66, 0x20, 0x76},
		-1,
		nil,
	},
	{
		"fig",
		0,
		[]byte{0x23, 0x46, 0x49, 0x47},
		-1,
		nil,
	},
	{
		"xpm",
		0,
		[]byte{0x2f, 0x2a, 0x20, 0x58, 0x50, 0x4d, 0x20, 0x2a, 0x2f},
		-1,
		nil,
	},
}

var (
	htmlCommentRegex = regexp.MustCompile("(?i)<!--([\\s\\S]*?)-->")
	svgRegex         = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:<!doctype svg[^>]*>\s*)?<svg[^>]*>[^*]*<\/svg>\s*$`)
	base64imgReg     = regexp.MustCompile("^data[:]image[/](.+?);base64,(.+?)$")
)

// IsValidImgFile 判断文件是否为有效图片
// 判断方式为通过文件的magic number，由于扩展名可以随意修改，参考价值不大
// 参考如下资料：
// https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5
// https://datatracker.ietf.org/doc/html/draft-zern-webp
// https://github.com/ImageMagick/ImageMagick/issues/4230
// svg的判断方法为正则
// 参考如下资料：
// https://github.com/h2non/go-is-svg
func IsValidImgFile(path string) (bool, string) {
	// 读取文件前100字节用于判断magic number
	path = strings.TrimSpace(path)
	if "" == path {
		return false, ""
	}
	st, e := os.Stat(path)
	if nil != e || st.IsDir() || 0 == st.Size() {
		return false, ""
	}
	file, e := os.Open(path)
	if nil != e {
		return false, ""
	}
	defer file.Close()
	buffer := make([]byte, 100)
	readSize, e := file.Read(buffer)
	if (nil != e && !errors.Is(e, io.EOF)) || 0 == readSize {
		return false, ""
	}
	buffer = buffer[:readSize]

	// 根据配置逐项匹配，如果同一配置中有多个字节组，则需要全部匹配才算匹配成功
	succ, imgType := isValidImgByFrontBytes(buffer)
	if succ {
		return true, imgType
	}

	// 如果上面方式没匹配，则读取全部字节后判断是否为svg
	buffer, e = os.ReadFile(path)
	if nil != e && !errors.Is(e, io.EOF) {
		return false, ""
	}
	if succ, imgType := isValidImgByAllBytes(buffer); succ {
		return true, imgType
	}
	return false, ""
}

func isValidImgByFrontBytes(frontBytes []byte) (bool, string) {
	// 根据配置逐项匹配，如果同一配置中有多个字节组，则需要全部匹配才算匹配成功
	for _, meta := range metas {
		if matchBytes(frontBytes, &meta) {
			return true, meta.imgType
		}
	}
	return false, ""
}

func isValidImgByAllBytes(all []byte) (bool, string) {
	if isSVG(all) {
		return true, "svg"
	}
	return false, ""
}

// isBinary checks if the given buffer is a binary file.
func isBinary(buf []byte) bool {
	if len(buf) < 24 {
		return false
	}
	for i := 0; i < 24; i++ {
		charCode, _ := utf8.DecodeRuneInString(string(buf[i]))
		if charCode == 65533 || charCode <= 8 {
			return true
		}
	}
	return false
}

// Is returns true if the given buffer is a valid SVG image.
func isSVG(buf []byte) bool {
	return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{}))
}

// 把文件前100字节与指定配置项匹配，配置中有多个字节组，则需要全部匹配才算匹配成功
func matchBytes(buffer []byte, meta *imgMeta) bool {
	match := func(buffer []byte, offset int, bytes []byte) bool {
		to := offset + len(bytes)
		if len(buffer) < to {
			return false
		}
		for i := offset; i < to; i++ {
			if buffer[i] != bytes[i-offset] {
				return false
			}
		}
		return true
	}

	if nil != meta.bytes {
		if !match(buffer, meta.offset, meta.bytes) {
			return false
		}
	}
	if nil != meta.bytes2 {
		if !match(buffer, meta.offset2, meta.bytes2) {
			return false
		}
	}
	return true
}

func GetBase64ImgInfo(str string, shouldValidate bool) (succ bool, imgType string, imgCont []byte) {
	defer func() {
		if e := recover(); nil != e {
			succ = false
			imgType = ""
			imgCont = nil
		}
	}()

	matches := base64imgReg.FindStringSubmatch(strings.TrimSpace(str))
	if len(matches) < 3 {
		return false, "", nil
	}
	bs, err := base64.StdEncoding.DecodeString(strings.TrimSpace(matches[2]))
	if nil != err {
		panic(err)
	}

	// 如果不验证，则使用图片base64串中的类型作为图片类型，同时也不验证内容的有效性
	if !shouldValidate {
		return true, matches[1], bs
	}

	if succ, imgType = isValidImgByFrontBytes(bs); succ {
		return true, imgType, bs
	}
	if succ, imgType := isValidImgByAllBytes(bs); succ {
		return true, imgType, bs
	}
	return false, "", nil
}

const (
	FIXED_EXT_JPG  = ".jpg"
	FIXED_EXT_JPEG = ".jpeg"
	FIXED_EXT_PNG  = ".png"
	FIXED_EXT_GIF  = ".gif"
)

var JPEG_ENCODE_OPTION = jpeg.Options{Quality: 100}

// EncodeImg 图片按扩展名编码到指定writer，如果扩展名未匹配到指定编码器，则默认使用jpg的
func EncodeImg(img image.Image, writer io.Writer, ext string) error {
	ext = strings.TrimSpace(strings.ToLower(ext))
	if FIXED_EXT_JPG == ext || FIXED_EXT_JPEG == ext {
		return jpeg.Encode(writer, img, &JPEG_ENCODE_OPTION)
	}
	if FIXED_EXT_PNG == ext {
		return png.Encode(writer, img)
	}
	if FIXED_EXT_GIF == ext {
		return gif.Encode(writer, img, nil)
	}
	return jpeg.Encode(writer, img, &JPEG_ENCODE_OPTION)
}

// EncodeImgToBytes 图片按扩展名编码为字节数组
func EncodeImgToBytes(img image.Image, ext string) ([]byte, error) {
	var buf bytes.Buffer
	err := EncodeImg(img, &buf, ext)
	if nil != err {
		return nil, err
	}
	return buf.Bytes(), nil
}
